<?php
// @formatter:off
/**
 * @file WCSearchContainer.php
 * @author Alejandro Dario Simi
 * @date $Date: 2013-07-18 22:13:05 +0000 (Thu, 18 Jul 2013) $
 *
 * $Id: WCSearchContainer.php 97 2013-07-18 22:13:05Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0.0.2/includes/search/WCSearchContainer.php $
 */
// @formatter:on

/**
 * @class WCSearchContainer This class is a container for search engine results.
 * It can hold a list of items and make some operations with them.
 */
class WCSearchContainer {
	/**
	 * @var int Group id of the first item added to this container.
	 */
	protected $_gid = false;
	/**
	 * @var mixed[string] List of items held by this container.
	 */
	protected $_items = array();
	/**
	 * @var string[string] This list hold a list of hash ids for each item
	 * type.
	 */
	protected $_itemsByType = array();
	/**
	 * @var int Number of elements being held by this container.
	 */
	protected $_itemsCount = 0;
	/**
	 * @var boolean This flag indicates if the list of items is sorted or
	 * not.
	 */
	protected $_sorted = true;
	/**
	 * @var string[] List of words used to find the items of this container.
	 */
	protected $_words = array();
	//
	// Public methods.
	/**
	 * This method adds a collection's search item to this container.
	 *
	 * @param int $collectionId Collection's ID.
	 * @param int $groupId Collection's group ID.
	 * @param string $word Word that generated this match.
	 * @param boolean $exact When this parameter is true, it means it was a
	 * exact match of the word.
	 * @param mixed $parent When item is generated depending on other, that
	 * other becomes its parent and it should be passed as parent.
	 * @return boolean Returns true when an item was successfully added.
	 */
	public function addCollection($collectionId, $groupId, $word = false, $exact = false, $parent = false) {
		//
		// Forwarding the operation specifying the item type.
		return $this->addItem("CL", $collectionId, $groupId, $word, $exact, $parent);
	}
	/**
	 * This method adds a comic's search item to this container.
	 *
	 * @param int $comicId Comic's ID.
	 * @param int $groupId Comic's group ID.
	 * @param string $word Word that generated this match.
	 * @param boolean $exact When this parameter is true, it means it was a
	 * exact match of the word.
	 * @param mixed $parent When item is generated depending on other, that
	 * other becomes its parent and it should be passed as parent.
	 * @return boolean Returns true when an item was successfully added.
	 */
	public function addComic($comicId, $groupId, $word = false, $exact = false, $parent = false) {
		//
		// Forwarding the operation specifying the item type.
		return $this->addItem("CX", $comicId, $groupId, $word, $exact, $parent);
	}
	/**
	 * This method adds a directory's search item to this container.
	 *
	 * @param int $directoryId Directory's ID.
	 * @param int $groupId Directory's group ID.
	 * @param string $word Word that generated this match.
	 * @param boolean $exact When this parameter is true, it means it was a
	 * exact match of the word.
	 * @param mixed $parent When item is generated depending on other, that
	 * other becomes its parent and it should be passed as parent.
	 * @return boolean Returns true when an item was successfully added.
	 */
	public function addDirectory($directoryId, $groupId, $word = false, $exact = false, $parent = false) {
		//
		// Forwarding the operation specifying the item type.
		return $this->addItem("DI", $directoryId, $groupId, $word, $exact, $parent);
	}
	/**
	 * This method adds a search item to this container.
	 *
	 * @param string $type Search item type.
	 * @param int $collectionId Collection's ID.
	 * @param int $groupId Collection's group ID.
	 * @param string $word Word that generated this match.
	 * @param boolean $exact When this parameter is true, it means it was a
	 * exact match of the word.
	 * @param mixed $parent When item is generated depending on other, that
	 * other becomes its parent and it should be passed as parent.
	 * @return boolean Returns true when an item was successfully added.
	 */
	public function addItem($type, $itemId, $groupId, $word = false, $exact = false, $parent = false) {
		//
		// Setting a default value to be returned.
		$added = false;
		//
		// When an item is added, sorting flag must be false.
		$this->_sorted = false;
		//
		// Saving group's ID.
		if($this->_gid === false) {
			$this->_gid = $groupId;
		}
		//
		// Checking the existence of a list of items for the current
		// type.
		if(!isset($this->_itemsByType[$type])) {
			$this->_itemsByType[$type] = array();
		}
		//
		// If the item id is not in its list, it's added.
		if(!in_array($itemId, $this->_itemsByType[$type])) {
			$this->_itemsByType[$type][] = $itemId;
		}
		//
		// Building a unique ID for the item.
		$id = $groupId.$type.$itemId;
		//
		// Checking if this item was already added to the container.
		if(!isset($this->_items[$id])) {
			//
			// Building the initial structure for the item and adding
			// it to the list.
			$this->_items[$id] = array(
				"factors" => array(
					"hits" => 0,
					"type" => WCSearchFactory::$TypePriority[$type],
					"e-words" => 0,
					"p-words" => 0
				),
				"type" => $type,
				"id" => $itemId,
				"group" => $groupId,
				"words" => array(
					"exact" => array(),
					"partial" => array()
				)
			);
			//
			// Incrementing item's count.
			$this->_itemsCount++;
			//
			// At this point, the item is considerer to be added.
			$added = true;
		}
		//
		// Incremented item's hits.
		$this->_items[$id]["factors"]["hits"]++;
		//
		// Checking if there's a matching word specified.
		if($word) {
			//
			// Checking which list should contain this word.
			$auxName = $exact ? "exact" : "partial";
			//
			// Checking if theres a counter for this word.
			if(!isset($this->_items[$id]["words"][$auxName][$word])) {
				$this->_items[$id]["words"][$auxName][$word] = 0;
			}
			//
			// Incrementing the right word counter.
			$this->_items[$id]["words"][$auxName][$word]++;
		}
		//
		// Checking if there's a parent specified.
		if($parent) {
			//
			// Copying word counts from its parent item.
			// @{
			foreach(array("exact","partial") as $match) {
				foreach($parent["words"][$match] as $word => $hits) {
					if(!isset($this->_items[$id]["words"][$match][$word])) {
						$this->_items[$id]["words"][$match][$word] = 0;
					}
					$this->_items[$id]["words"][$match][$word] += $hits;
				}
			}
			// @}
		}
		//
		// Returning the adding result.
		return $added;
	}
	/**
	 * This method adds a rule's search item to this container.
	 *
	 * @param int $ruleId Rule's ID.
	 * @param int $groupId Rule's group ID.
	 * @param string $word Word that generated this match.
	 * @param boolean $exact When this parameter is true, it means it was a
	 * exact match of the word.
	 * @param mixed $parent When item is generated depending on other, that
	 * other becomes its parent and it should be passed as parent.
	 * @return boolean Returns true when an item was successfully added.
	 */
	public function addRule($ruleId, $groupId, $word = false, $exact = false, $parent = false) {
		//
		// Forwarding the operation specifying the item type.
		return $this->addItem("RL", $ruleId, $groupId, $word, $exact, $parent);
	}
	/**
	 * This method adds a tag's search item to this container.
	 *
	 * @param int $tagId Tag's ID.
	 * @param int $groupId Tag's group ID.
	 * @param string $word Word that generated this match.
	 * @param boolean $exact When this parameter is true, it means it was a
	 * exact match of the word.
	 * @param mixed $parent When item is generated depending on other, that
	 * other becomes its parent and it should be passed as parent.
	 * @return boolean Returns true when an item was successfully added.
	 */
	public function addTag($tagId, $groupId, $word = false, $exact = false, $parent = false) {
		//
		// Forwarding the operation specifying the item type.
		return $this->addItem("TA", $tagId, $groupId, $word, $exact, $parent);
	}
	/**
	 * This method allows to know how many items are being held by this
	 * container.
	 *
	 * @return int Returns the amount of elements of this container.
	 */
	public function count() {
		return $this->_itemsCount;
	}
	/**
	 * This method allows to remove items from this container based on
	 * another container.
	 *
	 * @param WCSearchContainer $minus Every element in this container
	 * indicates which elements has to be removed.
	 * @return int returns the final amount of elements in the current
	 * container.
	 */
	public function filter(WCSearchContainer &$minus) {
		//
		// Checking all the items that have to be removed.
		foreach($minus->_items as $id => $item) {
			//
			// Checking if the item is held by this container.
			if(isset($this->_items[$id])) {
				//
				// Type's shortcut.
				$type = $this->_items[$id]["type"];
				//
				// Searching item's ID in the list of its type.
				$key = array_search($id, $this->_itemsByType[$type]);
				//
				// Removing item's ID from the list of its type.
				unset($this->_itemsByType[$type][$key]);
				//
				// Removing the actual item.
				unset($this->_items[$id]);
			}
		}
		//
		// Updating current amount of items and returning it.
		return $this->_itemsCount = count($this->_items);
	}
	/**
	 * This item allows to get the saved group ID.
	 *
	 * @return int Returns a group ID.
	 */
	public function gid() {
		return $this->_gid;
	}
	/**
	 * This method allows to get a specific item.
	 *
	 * @param string $type Item type.
	 * @param int $itemId Item ID.
	 * @param int $groupId Item's group ID.
	 * @return mixed Returns the requested item unless it's not present in
	 * the container, is such case it returns false.
	 */
	public function item($type, $itemId, $groupId) {
		//
		// Building the item hash ID.
		$id = $groupId.$type.$itemId;
		//
		// Checking item's presence and returning it.
		return isset($this->_items[$id]) ? $this->_items[$id] : false;
	}
	/**
	 * This method allows to get the full list of items held by this container.
	 *
	 * @return mixed[string] Returns a list of items.
	 */
	public function items() {
		//
		// Sorting items.
		$this->sort();
		//
		// Returning the internal list of items.
		return $this->_items;
	}
	/**
	 * This method sets the list of words used to find this container's items.
	 *
	 * @param string[] $words List of words.
	 */
	public function setWords(&$words) {
		$this->_words = $words;
	}
	/**
	 * This method allows to access a list of ids of certain type.
	 *
	 * @param string $type Type to be returned.
	 * @return string[] Returns a list of hash ids. If the certain type has
	 * no list yet, it returns an empty list.
	 */
	public function typeIds($type) {
		return isset($this->_itemsByType[$type]) ? $this->_itemsByType[$type] : array();
	}
	//
	// Protected methods.
	/**
	 * This method calculates and updates extra factors of each element.
	 */
	protected function calculateFactors() {
		//
		// Walking over each element.
		foreach($this->_items as $id => $item) {
			//
			// Setting a default value.
			$factor = 0;
			//
			// Counting how many times a searching word was hit
			// exactly.
			foreach($item["words"]["exact"] as $word => $hits) {
				$factor += $hits;
				//$factor += $hits * strlen($word);
			}
			//
			// Updating factor.
			$this->_items[$id]["factors"]["e-words"] = $factor;
			//
			// Setting a default value.
			$factor = 0;
			//
			// Counting how many times a searching word was hit
			// at least partially.
			foreach($item["words"]["partial"] as $word => $hits) {
				$factor += $hits;
				//$factor += $hits * strlen($word);
			}
			//
			// Updating factor.
			$this->_items[$id]["factors"]["p-words"] = $factor;
		}
	}
	/**
	 * This method sorts the internal list of items based on their factors.
	 */
	protected function sort() {
		//
		// Checking if the list is already sorted.
		if(!$this->_sorted) {
			//
			// Updating item's factors.
			$this->calculateFactors();
			//
			// Sorting elements.
			uasort($this->_items, 'WCSearchContainer::Compare');
			//
			// Setting sorting flag as true.
			$this->_sorted = true;
		}
	}
	//
	// Public class methods.
	/**
	 * This method allows to know which item is more important that other. In
	 * other words, which one goes first in search result.
	 *
	 * @param mixed $a Left operand of a comparison.
	 * @param mixed $b Right operand of a comparison.
	 * @return int Returns a zero when both items are equal, a positive
	 * value if $b is more important than $a, and a negative value in the
	 * other case.
	 */
	public static function Compare(&$a, &$b) {
		//
		// Setting a default value to be returned.
		$out = 0;
		//
		// Creating factors information shortcuts.
		// @{
		$aFactors = $a["factors"];
		$bFactors = $b["factors"];
		// @}
		//
		// Checking factors.
		// @{
		if($aFactors["e-words"] != $bFactors["e-words"]) {
			//
			// Here, exact words factor give the order.
			$out = $aFactors["e-words"] > $bFactors["e-words"] ? -1 : 1;
		} elseif($aFactors["p-words"] != $bFactors["p-words"]) {
			//
			// Here, partial words factor give the order.
			$out = $aFactors["p-words"] > $bFactors["p-words"] ? -1 : 1;
		} elseif($aFactors["hits"] != $bFactors["hits"]) {
			//
			// Here, hits give the order.
			$out = $aFactors["hits"] > $bFactors["hits"] ? -1 : 1;
		} elseif($aFactors["type"] != $bFactors["type"]) {
			//
			// Here, type gives the order.
			$out = $aFactors["type"] > $bFactors["type"] ? -1 : 1;
		}
		// @}
		//
		// Returning comparison result.
		return $out;
	}
}
?>